package net.uman.framework.modules.search;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.uman.entity.IdEntity;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;
import org.springframework.util.Assert;

public class LuceneSearch implements Search {
	private File rootDir;
	
	private Map<String, IndexWriter> indexWriters = new HashMap<String, IndexWriter>();

	public LuceneSearch(File rootDir) {
		this.rootDir = rootDir;
		if (!rootDir.exists()) {
			rootDir.mkdirs();
		}
	}

	private Analyzer getAnalyzer() {
		return new StandardAnalyzer(Version.LUCENE_30);
//		return new MMAnalyzer();
//		return new PaodingAnalyzer();
//		return new IKAnalyzer();
//		return new MMSegAnalyzer();
	}
	
	private synchronized void openIndex(String table) throws IOException {
		Analyzer luceneAnalyzer = getAnalyzer();
		// 第三个参数如果是true，在建立索引之前先将index目录清空
		File dir = new File(rootDir, table);
		
		try {
			if (IndexWriter.isLocked(new SimpleFSDirectory(dir))) {
				IndexWriter.unlock(new SimpleFSDirectory(dir));
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}

		IndexWriter indexWriter = new IndexWriter(new SimpleFSDirectory(dir), luceneAnalyzer, IndexWriter.MaxFieldLength.LIMITED);
		indexWriter.setMaxFieldLength(100000);
		indexWriter.setMaxBufferedDocs(1000);
		indexWriter.optimize();
		
		indexWriters.put(table, indexWriter);
		
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				try {
					LuceneSearch.this.closeIndex();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		});
	}
	
	public synchronized void clear(String table) {
		try {
			File dir = new File(rootDir, table);
			
			Analyzer luceneAnalyzer = getAnalyzer();
			// 第三个参数如果是true，在建立索引之前先将index目录清空
			try {
				if (IndexWriter.isLocked(new SimpleFSDirectory(dir))) {
					IndexWriter.unlock(new SimpleFSDirectory(dir));
				}
			} catch (Exception ex) {
				ex.printStackTrace();
			}
			IndexWriter indexWriter = new IndexWriter(new SimpleFSDirectory(dir), luceneAnalyzer, true, IndexWriter.MaxFieldLength.LIMITED);
			indexWriter.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/* (non-Javadoc)
	 * @see net.b2bcenter.framework.modules.search.Search#createIndex(java.lang.String, net.b2bcenter.framework.modules.search.FieldDesc[])
	 */
	public synchronized void createIndex(String table, String id, FieldDesc[] flds) throws IOException {
		Assert.notNull(flds, "Fields needed");
		
		
		checkOpen(table);
		
		deleteIndex(table, id);
		
		IndexWriter indexWriter = indexWriters.get(table); 
		Assert.notNull(indexWriter, "openIndex needed");
		
		// document对象，相当于数据库中一条记录
		Document document = new Document();
		
		Field f = new Field("id", id, Field.Store.YES, Field.Index.ANALYZED);
		document.add(f);
		
//		System.out.print("table:"+table+" id:"+id);
		// Field对象，相当于数据库中字段
		for (FieldDesc fld :  flds) {
			// Field.Index.ANALYZED 这就能进行索引了, 如果设置为NO的话就不能检索
			if (fld.getValue() != null) {
				f = new Field(fld.getName(), fld.getValue(), Field.Store.YES, fld.isSearchable() ? Field.Index.ANALYZED : Field.Index.NO);
				document.add(f);
			}
//			System.out.print(" "+fld.getName()+":"+fld.getValue());
		}
//		System.out.println();
		indexWriter.addDocument(document);
		indexWriter.commit();
	}
	
	public void deleteIndex(String table, String id) throws IOException {
		Criteria criteria = new Criteria();
		criteria.addCondition(new Condition("id", Condition.EQUALS, id));
		deleteIndex(table, criteria);
	}
	
	public synchronized void deleteIndex(IdEntity obj) throws IOException {
		Criteria criteria = new Criteria();
		criteria.addCondition(new Condition("id", Condition.EQUALS, obj.getId()));
		deleteIndex(obj.getClass().getSimpleName(), criteria);
	}
	
	public synchronized void deleteIndex(String table, Criteria criteria) throws IOException{
		Assert.notNull(criteria, "Criteria needed");
		
		checkOpen(table);
	
		IndexWriter indexWriter = indexWriters.get(table);
		Assert.notNull(indexWriter, "openIndex needed");
		
		try {
			Query query = getQuery(criteria);
			indexWriter.deleteDocuments(query);
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}	
	
	public List<List<FieldDesc>> queryDate(String table, Criteria criteria) throws IOException {
		List<List<FieldDesc>> docs = new ArrayList<List<FieldDesc>>();
		
		IndexReader reader = null;
		try {
			reader = IndexReader.open(new SimpleFSDirectory(new File(rootDir, table)));
		} catch (FileNotFoundException ex) {
			// file not found, no data
			return docs;
		}

//	    if (normsField != null)
//	      reader = new OneNormsReader(reader, normsField);

	    Searcher searcher = new IndexSearcher(reader);
//	    Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
	    Query query;
		try {
			query = getQuery(criteria);
		} catch (ParseException e) {
			e.printStackTrace();
			return docs;
		}

	    // 设置返回的最大数目，就返回前100条
		TopDocs topDocs = searcher.search(query, null, 2000);
		ScoreDoc[] hits = topDocs.scoreDocs;
		if (hits != null) {
			for (ScoreDoc hit : hits) {
				Document doc = searcher.doc(hit.doc);
				List<Fieldable> _flds = doc.getFields();
				List<FieldDesc> descs = new ArrayList<FieldDesc>();
				for (Fieldable f : _flds) {
					FieldDesc desc = new FieldDesc();
					desc.setName(f.name());
					desc.setValue(f.stringValue());
					
					descs.add(desc);
				}
				docs.add(descs);
			}
		}
		searcher.close();
		return docs;
	}
	
	private Query getQuery(Criteria criteria) throws CorruptIndexException, IOException,
	   org.apache.lucene.queryParser.ParseException {
		String queryString = getQueryString(criteria);
//		System.out.println("queryString:"+queryString);

//	    Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
	    Analyzer analyzer = getAnalyzer();

//		BooleanClause.Occur[] clauses = { BooleanClause.Occur.SHOULD, BooleanClause.Occur.SHOULD };
//		Query query = MultiFieldQueryParser.parse(Version.LUCENE_30, queryString, flds, clauses, analyzer); 
		QueryParser queryParser = new QueryParser(Version.LUCENE_30, null, analyzer);
		Query query = queryParser.parse(queryString);
		return query;
	}	  
	
	private String getQueryString(Criteria criteria) {
		StringBuffer sb = new StringBuffer();
		
		if (criteria == null) {
			return null;
		}
		
		boolean first = true;
		List<Condition> conds = criteria.getConditions();
		for (Condition cond : conds) {
			
			if (first) {
				first = false;
			} else {
				if (criteria.getRelation() == Criteria.AND) {
					sb.append(" AND ");
				} else if (criteria.getRelation() == Criteria.OR) {
					sb.append(" OR ");
				} else {
					throw new RuntimeException("Unknown relation: "+criteria.getRelation());
				}
			}
			
			if (cond.getCriteria() != null) {
				String s = getQueryString(cond.getCriteria());
				sb.append("(").append(s).append(")");
				continue;
			}
			
			sb.append(cond.getField()).append(":");
			
			int op = cond.getOperator();
			switch(op) {
			case Condition.EQUALS:	
				Object value = cond.getValue();
				Assert.notNull(value, "Value is null for field: "+cond.getField());
				
				if (value.toString().contains(" ")) {
					sb.append("\"").append(value.toString()).append("\"");
				} else {
					sb.append(value.toString());
				}
				break;
			case Condition.LIKE:	
				value = cond.getValue();
				Assert.notNull(value, "Value is null for field: "+cond.getField());
				
				if (value.toString().contains(" ")) {
					sb.append("\"").append(value.toString()).append("*\"");
				} else {
					sb.append(value.toString()).append("*");
				}
				break;
			case Condition.BETWEEN:
				Object[] values = cond.getValues();
				Assert.notNull(values, "Value is null for field: "+cond.getField());
				Assert.isTrue(values.length == 2, "Two values needed for field: "+cond.getField());
				Assert.notNull(values[0], "Value is null for field: "+cond.getField());
				Assert.notNull(values[1], "Value is null for field: "+cond.getField());
				
				String s1 = getString(values[0]);
				String s2 = getString(values[1]);
				Assert.isTrue(!s1.contains(" "), "Space not allowed for field: "+cond.getField());
				Assert.isTrue(!s2.contains(" "), "Space not allowed for field: "+cond.getField());
				
				sb.append("[").append(s1).append(" TO ").append(s2).append("]");
				break;
			default:
				throw new RuntimeException("Unknown operator: "+op);
			}
		}
		
		return sb.toString();
	}
	
	synchronized void commitIndex(IndexWriter indexWriter) throws IOException {
		if (indexWriter != null) {
			indexWriter.commit();
		}
	}
	
	/* (non-Javadoc)
	 * @see net.b2bcenter.framework.modules.search.Search#closeIndex()
	 */
	public synchronized void closeIndex() throws IOException {
		for (IndexWriter indexWriter : indexWriters.values()) {
			if (indexWriter != null) {
				indexWriter.close();
				indexWriter = null;
			}
		}
		
		indexWriters.clear();
	}

	private synchronized void checkOpen(String table) throws CorruptIndexException, LockObtainFailedException, IOException {
		IndexWriter indexWriter = indexWriters.get(table);
		if (indexWriter == null) {		
			openIndex(table);
		}
	}
	
	private String getString(Object o) {
		if (o instanceof Date) {
			DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT);
			return fmt.format((Date)o); 
		}
		return o.toString();
	}

	public static void main(String[] args) {
		Criteria criteria = new Criteria();
		criteria.setRelation(Criteria.AND);
		criteria.addCondition(new Condition("name", Condition.EQUALS, "acx tas"));
		Criteria c = new Criteria();
		c.setRelation(Criteria.OR);
		c.addCondition(new Condition("age", Condition.EQUALS, "10 30"));
		c.addCondition(new Condition("age", Condition.EQUALS, "20"));
		Condition cond = new Condition();
		cond.setCriteria(c);
		criteria.addCondition(cond);
		
		String s = new LuceneSearch(new File(".")).getQueryString(criteria);
		System.out.println("s:"+s);
	}
}
